<!--
Copyright 2014 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="../appengine_module/components/net.html">
<link rel="import" href="../lib/update-util.html">
<link rel="import" href="ct-builder-failure.html">
<link rel="import" href="ct-step-failure.html">
<link rel="import" href="ct-failure-group.html">
<link rel="import" href="ct-builder-failure-group-data.html">
<link rel="import" href="ct-master-failure.html">
<link rel="import" href="ct-master-failure-group-data.html">
<link rel="import" href="ct-step-failure-group-data.html">
<link rel="import" href="ct-trooper-failure-group-data.html">
<link rel="import" href="ct-commit-list.html">

<script>
function CTFailures(repositories) {
  this.repositories = repositories;
  // Maps a tree id to an array of CTFailureGroups within that tree.
  this.failures = null;
  this.lastUpdateDate = null;
  this.alertsHistoryKey = null;
}

(function() {
'use strict';

// FIXME: This could potentially move onto CTStepFailureGroupData as it isn't relevant to
// trooper failures.
// Reverse sorting order, if a > b, return a negative number.
CTFailures.prototype._failureByTreeListComparator = function(tree, a, b) {
  if (tree === undefined)
    tree = 'chromium';

  // Always bubble master alerts to the top
  if (a.data.category =='master') {
    if (b.data.category == 'master')
      return 0;
    return -1;
  } else if (b.data.category == 'master') {
    return 1;
  }

  if (!a.data.commitList)
    return 1;
  if (!b.data.commitList)
    return -1;

  var rev_a = a.data.commitList.revisions;
  var rev_b = b.data.commitList.revisions;

  if (!rev_a || !Object.keys(rev_a).length) {
    if (!rev_b || !Object.keys(rev_b).length)
      return 0;
    return 1;
  } else if (!rev_b || !Object.keys(rev_b).length) {
    return -1;
  }

  // Prioritize the tree's revision, if they are unequal (else, fallback below)
  if (rev_a[tree] && rev_b[tree] &&
      rev_a[tree].last() != rev_b[tree].last()) {
    return rev_b[tree].last() - rev_a[tree].last();
  }

  // Compare other revisions in alphabetical order.
  var keys = Object.keys(rev_a).sort();
  for (var i = 0; i < keys.length; i++) {
    if (keys[i] == tree)  // Already taken care of, above.
      continue;

    var a_list = rev_a[keys[i]];
    var b_list = rev_b[keys[i]];
    if (!b_list)
      return -1;

    if (a_list.last() != b_list.last())
      return b_list.last() - a_list.last();
  }
  return 0;
};

// Updates the format of the alerts array to be easier to parse.
CTFailures.prototype._mungeAlerts = function(alerts) {
  alerts.forEach(function(failure) {
    // FIXME: Have the server store the actual failure type in a different
    // field instead of smashing it into the reason.
    if (failure.failureType) {
      // The server has been fixed.
    } else {
      if (failure.reason) {
        var parts = failure.reason.split(':');
        failure.reason = parts[0];
        failure.failureType = parts[1] || 'FAIL';
      } else {
        failure.failureType = 'UNKNOWN';
      }
    }
  });
};

CTFailures.prototype.update = function() {
  var annotationPromise = CTFailureGroup.fetchAnnotations();
  var alertsUrl = 'https://sheriff-o-matic.appspot.com/alerts';

  if (window.location.search) {
    var parts = window.location.search.split('&');
    parts.forEach(function(part) {
      var v = part.split('=');
      if (v[0].replace('?', "") == 'history') {
        alertsUrl = 'https://sheriff-o-matic.appspot.com/alerts-history/' + v[1];
      }
    });
  }

  this.rotations = {};
  return Promise.all([annotationPromise, net.json(alertsUrl),
      net.json('https://trooper-o-matic.appspot.com/alerts')]).then(function(data_array) {
    var annotations = data_array[0];
    var sheriff_data = data_array[1];
    var trooper_data = data_array[2];

    var newFailures = {};

    var lastPosted = (sheriff_data.last_posted || sheriff_data.date) * 1000;
    this.lastUpdateDate = new Date(lastPosted);
    this.alertsHistoryKey = sheriff_data.key;

    this._mungeAlerts(sheriff_data.alerts);
    // FIXME: Change builder_alerts to expose the alerts as a map instead of an array.
    var alertsByKey = {}
    sheriff_data.alerts.forEach(function(alert) {
      alertsByKey[alert.key] = alert;
    });
    // Update |failures| with the appropriate CTFailureGroup's for each
    // tree.
    sheriff_data.range_groups.forEach(function(rangeGroup) {
      this._processFailuresForRangeGroup(newFailures, rangeGroup, alertsByKey, annotations, sheriff_data.suspected_cls);
    }.bind(this));

    sheriff_data.stale_builder_alerts.forEach(function(alert) {
      this._processBuilderFailure(newFailures, alert, annotations);
    }.bind(this));

    // Sort failure groups so that newer failures are shown at the top
    // of the UI.
    Object.keys(newFailures, function (tree, failuresByTree) {
      failuresByTree.sort(this._failureByTreeListComparator.bind(this, tree));
    }.bind(this));

    this.failures = updateUtil.updateLeft(this.failures, newFailures);
    this._processTrooperFailures(newFailures, trooper_data);
  }.bind(this));
};

CTFailures.prototype._failureComparator = function(a, b) {
  if (a.step > b.step)
    return 1;
  if (a.step < b.step)
    return -1
  if (a.testName > b.testName)
    return 1;
  if (a.testName < b.testName)
    return -1
  return 0;
};

CTFailures.prototype._processTrooperFailures = function(all_failures,
                                                        trooper_data) {
  var health_failures = [];
  Object.keys(trooper_data, function(failureType, failuresByTree) {
    Object.keys(failuresByTree, function(tree, failure) {
      if (failure.should_alert) {
        health_failures.push(new CTFailureGroup('health',
            new CTTrooperFailureGroupData(failure.details, failure.url, failure,
                                          failureType, tree)));
      }
    });
  });

  var trooper_failures = [];
  var trooper_fyi_failures = [];
  Object.keys(all_failures, function(tree, failuresByTree) {
    // By default, we put non chromium and blink waterfalls into 'trooper-fyi'.
    // As other waterfalls become more stable, they can be moved to 'trooper.'
    var failure_array = trooper_fyi_failures;
    if (tree == 'chromium' || tree == 'blink' || tree == 'chromium.perf') {
      failure_array = trooper_failures;
    }
    failuresByTree.forEach(function(failure) {
      if (failure.category == 'builder' || failure.category == 'master' || failure.category=='snoozed') {
        failure_array.push(failure);
      }
    });
  });

  this.failures['health'] = health_failures;
  this.failures['trooper'] = trooper_failures;
  this.failures['trooper-fyi'] = trooper_fyi_failures;
};

CTFailures.prototype._groupFailuresByTreeAndReason = function(failures, annotations) {
  var failuresByTree = {};
  failures.forEach(function(failure) {
    // Establish the key to uniquely identify a failure by reason.
    var failureKey = CTStepFailure.createKey(failure);

    var reasonKey = JSON.stringify({
      step: failure.step_name,
      reason: failure.reason,
    });

    // FIXME: Use a model class instead of a dumb object.
    if (!failuresByTree[failure.tree])
      failuresByTree[failure.tree] = {};
    if (!failuresByTree[failure.tree][reasonKey])
      failuresByTree[failure.tree][reasonKey] = {};
    failuresByTree[failure.tree][reasonKey][failure.builder_name] = {
      key: failureKey,
      // FIXME: Rename to failureType.
      actual: failure.failureType,
      lastFailingBuild: failure.last_failing_build,
      masterUrl: failure.master_url,
      failingBuildCount: (1 + failure.last_failing_build - failure.failing_build),
      annotation: annotations[failureKey],
      isTreeCloser: failure.would_close_tree,
    };
  });
  return failuresByTree
};

CTFailures.prototype._populateSuspectedCLs = function(suspectedCLGroup, analysisResult, masterUrl, builderName, buildNumber) {
  analysisResult.suspected_cls.forEach(function(cl) {
    if (!suspectedCLGroup[cl.repo_name])
      suspectedCLGroup[cl.repo_name] = {};
    if (!suspectedCLGroup[cl.repo_name][cl.revision]) {
      suspectedCLGroup[cl.repo_name][cl.revision] = {
        'commitPosition': cl.commit_position,
        'revision': cl.revision,
        'builds': [],
      };
    }
    var buildUrl = masterUrl + '/builders/' + builderName + '/builds/' + buildNumber;
    suspectedCLGroup[cl.repo_name][cl.revision].builds.push(buildUrl);
  });
};

CTFailures.prototype._filterOutsuspectedCLForStepFailure = function(suspectedCLGroup, stepFailure, analysisResults) {
  var stepName = stepFailure.step;

  // FIXME: filter with test name when Findit supports test-level analysis.
  Object.keys(stepFailure.resultNodesByBuilder, function(builderName, failure) {
    analysisResults.forEach(function(analysisResult) {
      if (stepName != analysisResult.step_name ||
          failure.lastFailingBuild != analysisResult.build_number ||
          builderName != analysisResult.builder_name ||
          failure.masterUrl != analysisResult.master_url)
        return;

      this._populateSuspectedCLs(suspectedCLGroup, analysisResult, failure.masterUrl, builderName, failure.lastFailingBuild);
    }.bind(this));
  }.bind(this));
};

CTFailures.prototype._processAnalysisResultsForStepFailureGroup = function(failures, analysisResults) {
  if (Object.isEmpty(analysisResults))
    return {};

  var suspectedCLGroup = {};
  failures.forEach(function(stepFailure) {
    this._filterOutsuspectedCLForStepFailure(suspectedCLGroup, stepFailure, analysisResults);
  }.bind(this));

  var suspectedCLsByRepo = {};
  Object.keys(suspectedCLGroup, function(repoName, clByRevision) {
    Object.keys(clByRevision, function(revision, cl) {
      if (!suspectedCLsByRepo[repoName])
        suspectedCLsByRepo[repoName] = [];
      suspectedCLsByRepo[repoName].push(cl);
    });
  });

  return suspectedCLsByRepo;
};

CTFailures.prototype._processFailuresForRangeGroup = function(newFailures, rangeGroup, alerts, annotations, analysisResults) {
  // A rangeGroup may be related to multiple alerts (via |failure_keys|). Categorize
  // these failures by reason (cause of failure), so that they can be grouped in the UI
  // (via a CTFailureGroup).
  var failures = rangeGroup.failure_keys.map(function(failure_key) {
    return alerts[failure_key];
  });
  var failuresByTree = this._groupFailuresByTreeAndReason(failures, annotations);

  if (Object.isEmpty(failuresByTree))
    return;

  Object.keys(failuresByTree, function(tree, resultsByReason) {
    var treeFailures = [];
    Object.keys(resultsByReason, function(reasonKey, resultsByBuilder) {
      var failure = JSON.parse(reasonKey);
      treeFailures.push(
          new CTStepFailure(failure.step, failure.reason, resultsByBuilder));
    });
    treeFailures.sort(this._failureComparator);

    var suspectedCLsByRepo = this._processAnalysisResultsForStepFailureGroup(treeFailures, analysisResults);

    if (!newFailures[tree])
      newFailures[tree] = [];
    var commitList = new CTCommitList(this.repositories, rangeGroup.likely_revisions);
    newFailures[tree].push(new CTFailureGroup(tree, new CTStepFailureGroupData(treeFailures, commitList, suspectedCLsByRepo)));
  }.bind(this));
};

CTFailures.prototype._processBuilderFailure = function(newFailures, alert, annotations) {
  var timeSinceLastUpdate = (this.lastUpdateDate.valueOf() / 1000) - alert.last_update_time;

  var failure;
  var data;
  if (alert.hasOwnProperty('builder_name')) {
    failure = new CTBuilderFailure(alert.tree, alert.master_url, alert.builder_name, alert.state,
        timeSinceLastUpdate, alert.pending_builds, alert.hasOwnProperty('isMaster'));
    data = new CTBuilderFailureGroupData(failure, alert.builder_name,
        alert.master_url + "/builders/" + alert.builder_name);
  } else {
    failure = new CTMasterFailure(alert.tree, alert.master_url, timeSinceLastUpdate);
    data = new CTMasterFailureGroupData(failure, alert.master_url);
  }
  data.annotation = annotations[failure.key];

  if (!newFailures[alert.tree])
    newFailures[alert.tree] = [];
  newFailures[alert.tree].push(new CTFailureGroup(alert.tree, data, 'builder'));
};

})();

</script>
